Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/next/pages/news/[id].tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2023 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Alert, Breadcrumb, Col, Layout, Radio, Row } from "antd";6import { GetServerSidePropsContext } from "next";7import { useRouter } from "next/router";8import NextHead from "next/head";9import dayjs from "dayjs";1011import { getNewsItemUserPrevNext } from "@cocalc/database/postgres/news";12import getCustomize from "@cocalc/database/settings/customize";13import { Icon } from "@cocalc/frontend/components/icon";14import { markdown_to_cheerio } from "@cocalc/frontend/markdown";15import { slugURL } from "@cocalc/util/news";16import { NewsPrevNext } from "@cocalc/util/types/news";1718import Footer from "components/landing/footer";19import Head from "components/landing/head";20import Header from "components/landing/header";21import A from "components/misc/A";22import { News } from "components/news/news";23import { NewsWithFuture } from "components/news/types";24import { useDateStr } from "components/news/useDateStr";25import Loading from "components/share/loading";26import { MAX_WIDTH, NOT_FOUND } from "lib/config";27import { Customize, CustomizeType } from "lib/customize";28import useProfile from "lib/hooks/profile";29import { extractID } from "lib/news";30import withCustomize from "lib/with-customize";3132interface Props {33customize: CustomizeType;34news: NewsWithFuture;35prev?: NewsPrevNext;36next?: NewsPrevNext;37metadata: {38title: string;39author: string;40url: string;41image: string;42published: string;43modified: string;44}45}4647const formatNewsTime = (newsDate: NewsWithFuture['date']) => (48typeof newsDate === "number" ? dayjs.unix(newsDate) : dayjs(newsDate)49).toISOString();5051export default function NewsPage(props: Props) {52const { customize, news, prev, next, metadata } = props;53const { siteName } = customize;54const router = useRouter();55const profile = useProfile({ noCache: true });56const isAdmin = profile?.is_admin;57const dateStr = useDateStr(news);58const permalink = slugURL(news);5960const title = `${news.title} – News – ${siteName}`;6162function future() {63if (news.future && !isAdmin) {64return (65<Alert type="info" banner={true} message="News not yet published" />66);67}68}6970function content() {71if (profile == null) return <Loading />;72if (!isAdmin && news.hide) {73return <Alert type="error" message="Not authorized" />;74}75if (isAdmin || !news.future) {76return <News news={news} showEdit={isAdmin} standalone />;77}78}7980function breadcrumb() {81const items = [82{ key: "/", title: <A href="/">{siteName}</A> },83{ key: "/news", title: <A href="/news">News</A> },84{85key: "permalink",86title: (87<A href={permalink}>88{isAdmin || (!news.future && !news.hide) ? (89<>90{dateStr}: {news.title}91</>92) : (93"Not Authorized"94)}95</A>96),97},98];99return <Breadcrumb items={items} />;100}101102function olderNewer() {103return (104<Radio.Group buttonStyle="outline" size="small">105<Radio.Button106disabled={!prev}107style={{ userSelect: "none" }}108onClick={() => {109prev && router.push(slugURL(prev));110}}111>112<Icon name="arrow-left" /> Older113</Radio.Button>114<Radio.Button115style={{ userSelect: "none" }}116onClick={() => {117router.push("/news");118}}119>120<Icon name="arrow-up" /> Overview121</Radio.Button>122<Radio.Button123disabled={!next}124style={{ userSelect: "none" }}125onClick={() => {126next && router.push(slugURL(next));127}}128>129<Icon name="arrow-right" /> Newer130</Radio.Button>131</Radio.Group>132);133}134135function renderTop() {136return (137<Row justify="space-between" gutter={15} style={{ margin: "30px 0" }}>138<Col>{breadcrumb()}</Col>139<Col>{olderNewer()}</Col>140</Row>141);142}143144return (145<Customize value={customize}>146<Head title={title} />147<NextHead>148<meta property="og:type" content="article"/>149150<meta property="og:title" content={metadata.title}/>151<meta property="og:url" content={metadata.url}/>152<meta property="og:image" content={metadata.image}/>153154<meta property="article:published_time" content={metadata.published}/>155<meta property="article:modified_time" content={metadata.modified}/>156</NextHead>157<Layout>158<Header/>159<Layout.Content160style={{161backgroundColor: "white",162}}163>164<div165style={{166minHeight: "75vh",167maxWidth: MAX_WIDTH,168padding: "30px 15px",169margin: "0 auto",170}}171>172{renderTop()}173{future()}174{content()}175</div>176<Footer />177</Layout.Content>178</Layout>179</Customize>180);181}182183export async function getServerSideProps(context: GetServerSidePropsContext) {184const { query } = context;185const id = extractID(query.id);186if (id == null) return NOT_FOUND;187188try {189const { news, prev, next } = await getNewsItemUserPrevNext(id);190const { siteName, siteURL } = await getCustomize();191192if (news == null) {193throw new Error(`not found`);194}195196// Extract image URL from parsed Markdown. By converting to HTML first, we197// automatically add support for HTML that's been embedded into Markdown.198//199const $markdown = markdown_to_cheerio(news.text)200const imgSrc = $markdown('img')201.first()202.attr('src');203204// Format published time205//206const publishedTime = formatNewsTime(news.date);207208// Get the last-modified time by sorting the post history by timestamp,209// reversing it, and parsing the first element in that array.210//211const newsModificationTimestamps = Object.keys(news.history || {})212.map(Number)213.filter((ts) => !Number.isNaN(ts))214.sort()215.reverse();216217const modifiedTime = newsModificationTimestamps.length218? formatNewsTime(newsModificationTimestamps[0])219: publishedTime;220221const metadata: Props['metadata'] = {222title: news.title,223url: `${siteURL}${slugURL(news)}`,224image: imgSrc || '',225published: publishedTime,226modified: modifiedTime,227author: `${siteName}`,228};229230return await withCustomize({231context,232props: {233news,234prev,235next,236metadata,237},238});239} catch (err) {240console.warn(`Error getting news with id=${id}`, err);241}242243return NOT_FOUND;244}245246247